package main

import (
    "encoding/json"
    "fmt"
    "github.com/boltdb/bolt"
    "sort"
    "strconv"
    "time"
)

type Store interface {
    SaveAction(c ...*Action) error //save之后获取 offset，由数据库之类的确保
    LoadActions(from, to uint64) ([]*Action, error)

    LoadNamespaces(at uint64) ([]*Namespace, error)
    SaveNamespaces(at uint64, ns []*Namespace) error
    DeleteNamespaces(at uint64) error
    Namespaces() ([]uint64, error)

    Offset() (uint64, error)

    Open() error
    Close() error
}

type BoltStore struct {
    dbfile string
    db     *bolt.DB
}

func (bs *BoltStore) Close() error {
    if bs.db == nil {
        return nil
    }
    return bs.db.Close()
}

func (bs *BoltStore) Open() error {
    db, err := bolt.Open(bs.dbfile, 0600, nil)
    if err == nil {
        bs.db = db
    }
    bs.db.Update(func(tx *bolt.Tx) error {
        metaBucket, err := tx.CreateBucketIfNotExists([]byte(StoreMetaBucket))
        if err != nil {
            return err
        }
        s := metaBucket.Get([]byte(ActionOffsetKey))
        if len(s) == 0 {
            return metaBucket.Put([]byte(ActionOffsetKey), []byte("0"))
        }
        return nil
    })
    return err
}

const (
    StoreActionBucket = "__actions__" //offset->action
    StoreMetaBucket   = "__meta__"    //
    ActionOffsetKey   = "__action_offset__"
)

/*

*/
func (bs *BoltStore) SaveAction(actions ...*Action) error {
    if bs.db == nil {
        return StoreNotInitializeError
    }
    tx, err := bs.db.Begin(true)
    if err != nil {
        return err
    }
    defer tx.Rollback()
    actionBucket, err := tx.CreateBucketIfNotExists([]byte(StoreActionBucket))
    if err != nil {
        return err
    }
    metaBucket, err := tx.CreateBucketIfNotExists([]byte(StoreMetaBucket))
    if err != nil {
        return err
    }
    var offset uint64
    s := metaBucket.Get([]byte(ActionOffsetKey))
    offset, err = strconv.ParseUint(string(s), 10, 64)
    if err != nil {
        return err
    }
    for _, action := range actions {
        offset = offset + 1
        action.Offset = offset
        action.CreateTime = time.Now().UnixNano()
        b, err := action.Value()
        if err != nil {
            return err
        }
        k := []byte(fmt.Sprintf("%d", offset))
        err = actionBucket.Put(k, b)
        if err != nil {
            return err
        }
    }
    err = metaBucket.Put([]byte(ActionOffsetKey), []byte(fmt.Sprintf("%d", offset)))
    if err != nil {
        return err
    }
    err = tx.Commit()
    if err != nil {
        for _, action := range actions {
            action.Offset = 0
            action.CreateTime = 0
        }
    }
    return err
}

//[from,to]
func (bs *BoltStore) LoadActions(from, to uint64) ([]*Action, error) {
    if bs.db == nil {
        return nil, StoreNotInitializeError
    }
    var ret []*Action
    err := bs.db.View(func(tx *bolt.Tx) error {
        actionBucket := tx.Bucket([]byte(StoreActionBucket))
        if actionBucket == nil {
            return fmt.Errorf("buckect %s not exist", StoreActionBucket)
        }
        return actionBucket.ForEach(func(k, v []byte) error {
            o, err := strconv.ParseUint(string(k), 10, 64)
            if err != nil {
                return err
            }
            if to >= o && o >= from {
                action := &Action{}
                err := action.Load(v)
                if err != nil {
                    return err
                }
                ret = append(ret, action)
            }
            return nil
        })
    })
    sort.Sort(Actions(ret))
    return ret, err
}

func (bs *BoltStore) Offset() (a uint64, err error) {
    if bs.db == nil {
        err = StoreNotInitializeError
        return
    }
    err = bs.db.Batch(func(tx *bolt.Tx) error {
        b, err := tx.CreateBucketIfNotExists([]byte(StoreMetaBucket))
        if err != nil {
            return err
        }
        s := b.Get([]byte(ActionOffsetKey))
        if len(s) == 0 {
            a = 0
            return nil
        }
        if a, err = strconv.ParseUint(string(s), 10, 64); err != nil {
            return err
        }
        return nil
    })
    return
}

func (bs *BoltStore) Namespaces() (ret []uint64, err error) {
    if bs.db == nil {
        err = StoreNotInitializeError
        return
    }
    err = bs.db.View(func(tx *bolt.Tx) error {
        return tx.ForEach(func(name []byte, b *bolt.Bucket) error {
            i, err := strconv.ParseUint(string(name), 10, 64)
            if err == nil {
                ret = append(ret, i)
            }
            return nil
        })
    })
    sort.Sort(Offsets(ret))
    return
}
func (bs *BoltStore) DeleteNamespaces(at uint64) error{
    if bs.db == nil {
        return  StoreNotInitializeError
    }
    return bs.db.Update(func(tx *bolt.Tx) error {
        return tx.DeleteBucket([]byte(fmt.Sprintf("%d",at)))
    })
}

func (bs *BoltStore) LoadNamespaces(at uint64) (ret []*Namespace, err error) {
    if bs.db == nil {
        return nil, StoreNotInitializeError
    }

    err = bs.db.View(func(tx *bolt.Tx) error {
        b := tx.Bucket([]byte(fmt.Sprintf("%d", at)))
        if b == nil {
            return fmt.Errorf("store at offset:%d not exsit", at)
        }
        return b.ForEach(func(k, v []byte) error {
            var ns = &Namespace{}
            if err := json.Unmarshal(v, ns); err != nil {
                return err
            }
            if string(k) != ns.Name {
                return fmt.Errorf("save ns name: %s not mtach with data: %s", string(k), ns.Name)
            }
            ret = append(ret, ns)
            return nil
        })
    })
    return ret, err
}

var StoreNotInitializeError = fmt.Errorf("store not initialized")

func (bs *BoltStore) SaveNamespaces(at uint64, ns []*Namespace) error {
    if bs.db == nil {
        return StoreNotInitializeError
    }
    tx, err := bs.db.Begin(true)
    if err != nil {
        return err
    }
    defer tx.Rollback()
    bName := []byte(fmt.Sprintf("%d", at))
    if b := tx.Bucket(bName); b != nil {
        return fmt.Errorf("buckect %d exsiting", at)
    }
    bucket, err := tx.CreateBucket(bName)
    if err != nil {
        return err
    }
    for _, n := range ns {
        k := []byte(n.Name)
        v, err := json.Marshal(n)
        if err != nil {
            return err
        }
        if err = bucket.Put(k, v); err != nil {
            return err
        }
    }
    return tx.Commit()
}
